// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Serializer.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.IO
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Xml;
    using Microsoft.Robotics.IO;    

    /// <summary>
    /// A class for writing and reading objects to disk
    /// </summary>
    public static class Serializer
    {
        /// <summary>
        /// Defines the maximum number of objects in the array we support
        /// We limit the size to catch bugs where an array type is not serialized as a compact array.
        /// This size allows for Maps that are 50000x50000
        /// </summary>
        public const int MaxItemsInTheObjectGraph = 1024 * 1024;

        /// <summary>
        /// Opens a file and writes an object to it.
        /// </summary>
        /// <typeparam name="T">The object type to write</typeparam>
        /// <param name="fileName">The file name to write to.</param>
        /// <param name="objectToSave">The object to save</param>
        /// <param name="useSerializableArray">
        /// Determines whether to user SerializableArray type for array.
        /// This is usually true for all files which are not manually inspected or changed - for example the file recorders.
        /// Some files like the camera calibration parameters need to be manually populated from data generated from other tools,
        /// hence it is necessary to keep arrays in human readable form.
        /// </param>
        public static void Save<T>(string fileName, T objectToSave, bool useSerializableArray = true)
        {
            Serializer.Save<T>(fileName, objectToSave, (IEnumerable<Type>)null, useSerializableArray);
        }

        /// <summary>
        /// Opens a file and writes an object to it.
        /// </summary>
        /// <typeparam name="T">The object type to write</typeparam>
        /// <param name="fileName">The file name to write to.</param>
        /// <param name="objectToSave">The object to save</param>
        /// <param name="knownTypes">The set of known types needed for serialization.</param>
        /// <param name="useSerializableArray">
        /// Determines whether to user SerializableArray type for array.
        /// This is usually true for all files which are not manually inspected or changed - for example the file recorders.
        /// Some files like the camera calibration parameters need to be manually populated from data generated from other tools,
        /// hence it is necessary to keep arrays in human readable form.
        /// </param>
        public static void Save<T>(string fileName, T objectToSave, IEnumerable<Type> knownTypes, bool useSerializableArray = true)
        {
            DataContractSerializer serializer = new DataContractSerializer(
                objectToSave.GetType(), 
                knownTypes, 
                MaxItemsInTheObjectGraph, 
                true, 
                false,
                new SerializationSurrogate(true, useSerializableArray));

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.NamespaceHandling = NamespaceHandling.OmitDuplicates;
            using (XmlWriter writer = XmlTextWriter.Create(fileName, settings))
            {
                serializer.WriteObject(writer, objectToSave);
            }

            if (knownTypes != null)
            {
                serializer = new DataContractSerializer(
                typeof(Type[]),
                null,
                MaxItemsInTheObjectGraph,
                true,
                false,
                new SerializationSurrogate(true, useSerializableArray));

                // save a satellite file with the known types
                using (XmlWriter writer = XmlTextWriter.Create(TypesFileFromFileName(fileName), settings))
                {
                    serializer.WriteObject(writer, knownTypes.ToArray());
                }
            }
        }

        /// <summary>
        /// Opens a file and reads the one object it contains.
        /// </summary>
        /// <typeparam name="T">The object type to deserialize</typeparam>
        /// <param name="fileName">The file name to read from.</param>
        /// <param name="useSerializableArray">Determines whether to use serializable array type for array serialization.</param>
        /// <returns>The deserialized object</returns>
        public static T Load<T>(string fileName, bool useSerializableArray = true)
        {
            IEnumerable<Type> knownTypes = LoadKnownTypesForFile(fileName);

            // load the object if there is one
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                return Serializer.Load<T>(fileStream, knownTypes, useSerializableArray);
            }
        }

        /// <summary>
        /// Deserializes the object in the stream. Does not close the stream.
        /// </summary>
        /// <typeparam name="T">The object type to deserialize</typeparam>
        /// <param name="stream">The stream name to deserialize from.</param>
        /// <param name="knownTypes">The set of known types to deserialize</param>
        /// <param name="useSerializableArray">Determines whether to use serializable array type for array serialization.</param>
        /// <returns>The deserialized object</returns>
        public static T Load<T>(Stream stream, IEnumerable<Type> knownTypes, bool useSerializableArray = true)
        {
            // load the object if there is one
            DataContractSerializer serializer = new DataContractSerializer(typeof(T), knownTypes, MaxItemsInTheObjectGraph, true, false, new SerializationSurrogate(true, useSerializableArray));
            T result = (T)serializer.ReadObject(stream);
            return result;
        }

        /// <summary>
        /// Loads the know types for a given serialized file if a satellite file is present
        /// </summary>
        /// <param name="fileName">The name of the serialized file</param>
        /// <returns>The set of known types stored in the satellite file</returns>
        public static IEnumerable<Type> LoadKnownTypesForFile(string fileName)
        {
            Type[] knownTypes = null;

            // check for a satellite file containing known types
            string satelliteFileName = TypesFileFromFileName(fileName);
            if (File.Exists(satelliteFileName))
            {
                knownTypes = Load<Type[]>(satelliteFileName, true);
            }

            return knownTypes;
        }

        /// <summary>
        /// Returns the name of the satellite file containing known types
        /// </summary>
        /// <param name="fileName">The file to serialize to</param>
        /// <returns>The name of the satellite file</returns>
        private static string TypesFileFromFileName(string fileName)
        {
            return fileName + ".types";
        }
    }
}
